#!/usr/bin/perl -w
# Capture
# Manage packet captures without going crazy

# Grant Stavely - 4/2009
# gstavely@gmail.com

# This work is licensed under a 
# Creative Commons Attribution-Share Alike 3.0 United States License
# http://creativecommons.org/licenses/by-sa/3.0/us/

# You will need tcpdump, tshark, and capinfos, and modules listed below
# And you'll probably want daemonlogger 

#use warnings;
#use diagnostics;
use strict;
use sigtrap 'handler' => \&signal_handler, 'normal-signals';
use Exporter;
use Sys::Syslog;
use Getopt::Long; 
use Pod::Usage;
use POSIX qw(strftime);
use File::Copy;
use File::Basename;
use File::Path;
use File::Spec::Functions qw(:ALL);
use File::Temp qw(tempdir);
use Sys::Hostname;
use Digest::MD5 qw(md5_hex);
use Digest::SHA1 qw(sha1_hex);
Getopt::Long::Configure("bundling");

my $VERSION    = "cap v1.8";
my $HOSTNAME   = hostname();
my $PID	       = $$;

my %config = (
   interface	     => "eth0",
   active_cap_dir    => "/var/nsm/var/captures",
   rolling_cap_dir   => "/var/nsm/var/captures/ringbuffer",
   complete_cap_dir  => "/var/nsm/var/captures/complete",
   cap_extension     => "pcap",
   calc_hash	     => 1,
   verbose	     => 0,
   group	     => "",
   syslog	     => "1",
   rolling_prefix    => "full.$HOSTNAME",
   list_filter	     => 0,
   backwards	     => 1,
   sudo		     => 1,
   ignore_errors     => 1,
   tcpdump_flags     => "-q -nn -s 1516"
);

# Later we'll be parsing filenames so we need to keep filenames simple
my $SAFE_CHARACTERS ='-a-zA-Z0-9';


### MAIN ####
# No buffering.
select(STDERR);
$| = 1;
select(STDOUT);
$| = 1;

# Handy globals
my $timestamp  = strftime "%a.%b%e.%Y-%H.%M.%S.UTC", gmtime;
my $time       = time();

sub parse_cmdline($);
sub debug();
sub debug_print($);
sub verbose_print($); 
sub find_rolling();
sub generate_filenames($);
sub filter();
sub fixcap;
sub capinfo($);
sub start();
sub find();
sub list();
sub stop();
sub calc_hash($);
sub move_complete($);
sub fixup_permissions($);
sub log_dump($);
sub signal_handler();
sub unlink_tmp_files;
sub corrupt_or_exit;
sub clean_exit;
sub clean_up;
sub utils_convert_bytes_to_optimal_unit($);

# A hash of temp files to clean up if interrupted
my %tmp_files  = (
   complete_so_far   => "88mph",
   filtering	     => "",
   filtered	     => "",
   merging	     => "",
   merged	     => "",
   editing	     => "",
   edited	     => "",
   linked	     => "",
);
   

# Get to work	
parse_cmdline(\%config);

sub parse_cmdline($) {
   my $cfg_ref = shift;

   Getopt::Long::Configure("bundling");

   my $cmdline_ok = GetOptions(
      'h|help|?'	=> \$$cfg_ref{help}, 
      'm|man'       	=> \$$cfg_ref{man},
      'v|verbose+'     	=> \$$cfg_ref{verbose},
      'l|list'      	=> \$$cfg_ref{list},
      's|stop'      	=> \$$cfg_ref{stop},
      'a|analyst:s'     => \$$cfg_ref{analyst},
      'd|description:s' => \$$cfg_ref{description},
      'e|expression:s'  => \$$cfg_ref{bpf_expression},
      'f|filter:s'      => \$$cfg_ref{list_filter},
      'r|ringbuffer'    => \$$cfg_ref{filter},
      'c|checksum'      => \$$cfg_ref{calc_hash},
      'V|Version'   	=> \$$cfg_ref{versions},
      'z|debug'     	=> \$$cfg_ref{debug},
      'E|Errors'	=> \$$cfg_ref{ignore_errors},
   );
   pod2usage(-msg => $VERSION, -verbose =>1) unless ($cmdline_ok && $#ARGV == -1);

   # Santize config paths
   $config{active_cap_dir} = canonpath($config{active_cap_dir})
      if ($config{active_cap_dir});
   $config{rolling_cap_dir} = canonpath($config{rolling_cap_dir})
      if ($config{rolling_cap_dir});
   $config{complete_cap_dir} = canonpath($config{complete_cap_dir})
      if ($config{complete_cap_dir});

   # Sanitize input and replace invalid characters with '.'
   $config{analyst} =~ s/[^$SAFE_CHARACTERS]/\./go if ($config{analyst});
   $config{description} =~ s/[^$SAFE_CHARACTERS]/\./go if ($config{description});
   $timestamp =~ s/[^$SAFE_CHARACTERS]/\./go;
}

# What to do?
debug() 
   if ($config{debug});
filter() 
   if ($config{filter});
start() 
   if ($config{start});
start()
   if ($config{description});
list() 
   if ($config{list});
stop()
   if ($config{stop});
pod2usage(-msg => $VERSION, -verbose =>1) 
   if ($config{help});
pod2usage(-verbose =>2) 
   if ($config{man});

sub debug() {
   print "Debug Options:\n";
   while(my ($k, $v) = each %config ) {
      print "  $k: $v\n" if ($v);
   }
   print "\n";
}

sub debug_print($) {
   return
      if (!$config{debug});
   my $debug = shift;
   print "\rDebug: $debug\n" if $config{debug};
}

sub verbose_print($) {
   return
      if (!$config{verbose});
   my $verbose = shift;
   print $verbose;
}

sub find_rolling() {
   debug_print("Finding rolling captures...");
   opendir ROLLINGCAPS, $config{rolling_cap_dir}
      or clean_exit($?,"could not open directory $config{rolling_cap_dir}");
   my @found_files = readdir ROLLINGCAPS;
   if ($config{backwards}) {
      @found_files = sort {$b cmp $a} @found_files;
   }
   else {
      @found_files = sort @found_files;
   }
   return @found_files;
}       

sub generate_filenames($) {
   my $tag = shift;
   if (!$config{analyst}) {
      pod2usage(-msw => $VERSION, -verbose => 1);
      clean_exit($?,"Error: refusing to start a capture without an anlyst's name");
   }
   if (!$config{description}) {
      pod2usage(-msw => $VERSION, -verbose => 1);
      clean_exit($?,"Error: refusing to start a capture without a description");
   }

   # Generate filename and absolute path
   my $outfile = join('_',
      $config{analyst},
      $config{description},
      $timestamp,
      $time,
      $PID,
      $config{cap_extension}
   );
   $outfile =~ s/($PID)_/$PID.$tag./
      if ($tag);
   my $abs_outfile	      = catfile($config{complete_cap_dir}, $outfile);
   my $abs_running_outfile    = catfile($config{active_cap_dir}, $outfile);
   my @filenames = ($outfile, $abs_outfile, $abs_running_outfile);
   return @filenames;
}

sub filter() {
   debug_print("Filtering...");
   my ($outfile, $abs_outfile, $abs_running_outfile) = generate_filenames("past");
   print "Generating $outfile\n";
   
   # Keep track of which files have issues
   my ($clean_files, $corrupt_files) = 0;

   # Away we go
   my @rolling_caps = find_rolling();
   foreach my $rolling_file (@rolling_caps) {
      # Snatch the timestamp daemonlogger appends to the end of the filename
      # and ignore random caps dumped in that directory, edits from other
      # cap jobs, and so on
      next unless ($rolling_file =~ m/$config{rolling_prefix}/);
      $rolling_file =~ /(\d+)$/;
      my $cap_slice = $1;
      # Tuck away the complete path and filename in case bad things happen 
      $tmp_files{filtering} = $abs_running_outfile;
      $tmp_files{filtering} =~ s/$PID/filtering.$PID/;
      my $abs_rolling_file = catfile($config{rolling_cap_dir}, $rolling_file);
      # Rolling captures will constantly be disapearing out from under us
      unless (-r "$abs_rolling_file") {
	 verbose_print ("I can't read $abs_rolling_file, and have to assume that it rolled over\n");
	 next;
      }
      print "\rCurrently Processing: " . scalar(localtime($cap_slice)) ."";
        
      # filter the file with tcpdump
      my @read_command = (
	 "tcpdump", 
	 $config{tcpdump_flags},
	 "-r", 
	 $abs_rolling_file,
	 "-w", 
	 $tmp_files{filtering}, 
	 $config{bpf_expression},
      );
      # If you'd like to hard code sudo, add it now
      unshift(@read_command, "sudo")
	 if ($config{sudo});
   
      debug_print("I'm about to filter: @read_command");
      #log_dump("Filtering: @read_command");
      # if this doesn't work, corrupt_or_exit is going to call
      # fixcap() against $abs_rolling_file, so let's tuck it away 
      # for now
      $tmp_files{editing} = $abs_rolling_file;
      system("@read_command &>/dev/null") == 0
	 # pass to a special handler instead of clean_exit 
	 # to check for corrupt rolling captures
	 or corrupt_or_exit(
	    $?,
	    "Error reading file... @read_command",
	    # corrupt files are possible, get what you can if so
	    $config{ignore_errors}
	 );
   
      # Tricky to catch if that was a corrupt exit or a successful dump
      # and if it was a corrupt exit, if it was fixed, 
      # so we'll ask our hash. Signals are fail.
      if ($tmp_files{edited}) {
	 $corrupt_files++;
	 # GOTO REFILTER - continue filtering the edited no-longer-corrupt file
	 my @reread_command = (
	    "tcpdump",
	    $config{tcpdump_flags},
	    "-r", 
	    "$tmp_files{edited}",,
	    "-w", 
	    # Write out to the same filtered file again
	    $tmp_files{filtering}, 
	    $config{bpf_expression},
	 );						
      system("@reread_command &>/dev/null") == 0
	 # if we are still corrupt, exit 
	 or clean_exit(
	    $?,
	    "Error reading edited pcap @reread_command", 
	 );
      }
      
      # We must be OK
      else {
	 $clean_files++;
      }
   
      # Once done filtering, set the file aside to be merged
      $tmp_files{merging}  = $tmp_files{filtering};
      $tmp_files{merging}  =~ s/filtering.$PID/merging.$PID/;
      $tmp_files{merged}   = $tmp_files{merging};
      $tmp_files{merged}   =~ s/merging.$PID/merged.$PID/;
      # all of this is happening in the active cap dir
      # this shouldn't cost anything on local file systems - just a rename
      debug_print("I'm moving $tmp_files{filtering} to $tmp_files{merging}");
      system("mv", "-f", $tmp_files{filtering}, $tmp_files{merging}) == 0
	 or clean_exit(
	    $?,
	    "Unable to move $tmp_files{filtering} to $tmp_files{merging}",
	 );
   
      # Mergecap is really picky about getting bad files
      my @merge = (
	 "mergecap",
	 "-s 1516",
	 "-w",
	 $tmp_files{merged},
	 $tmp_files{merging}
      );
      # that was for the first run, if we've already processed one file
      # we want add that in as well
      push (@merge, $tmp_files{complete_so_far}) 
	 unless ($tmp_files{complete_so_far} eq "88mph");
   
      debug_print("I'm about to merge: @merge");
      system("@merge &>/dev/null")  == 0
	 or clean_exit(
	    $?,
	    "Error merging: @merge",
	 );
	 
      # Save current progress 
      system ("mv", $tmp_files{merged}, $abs_outfile) == 0
	 or clean_exit(
	    $?,
	    "Unable to move $tmp_files{merged} to $abs_outfile",
	 );
      $tmp_files{complete_so_far} = $abs_outfile;
   
      # Update the final file as a link to how far along we are
      my @linker = (
	 "ln",
	 "-sf",
	 $tmp_files{complete_so_far},
	 $abs_running_outfile
      );
   
      system (@linker) == 0
	 or clean_exit(
	    $?,
	    "Error maintaining soft link @linker: $!"
	 );
      # remember that there is now a linked file to get rid of
      $tmp_files{linked} = $abs_running_outfile;
   
      # Perms will pass through to the merged file, should allow users
      # to begin analyzing without further sudo commands
      fixup_permissions($tmp_files{linked});
      
      # Once this round is done, unset some of our temp variables
      # so that they don't clobber the next pass
      # We only want to keep $tmp_files{complete_so_far}
      $tmp_files{filtering} = undef;
      $tmp_files{filtered} = undef;
      #$tmp_files{merging} = undef;
      $tmp_files{merged} = undef;
      $tmp_files{editing} = undef;
      $tmp_files{edited} = undef;
   }	

   # Now that all processing is done, fix permissions 
   fixup_permissions($abs_outfile);
   # and clean up
   clean_up()
}

sub fixcap {
   $tmp_files{edited} =~ "$tmp_files{editing}.edited";
   print "\r\n\tCorrupt file: $tmp_files{editing}, grabbing what I can\n";
   my @editcap = (
      "editcap",
      "$tmp_files{editing}",
      "$tmp_files{edited}",
   );
   debug_print("\nI'm about to edit: @editcap");
   #log_dump("Editing: @editcap");
   
   # editcap will always exit 1 but we do want to die here if signaled
   system("@editcap &>/dev/null") == 0
      or clean_exit(
	 $?,
	 "Expected editcap to die, killing source file",
	 # Always ignore errors here, regardless of $config{ignore_errors}
	 1,
   );

   # grab stats on broken and recovered file 
   my (undef, undef, undef, undef, undef, undef, undef,
      $rolling_bytes , undef, undef, undef, undef, undef) = stat($tmp_files{editing});
   my (undef, undef, undef, undef, undef, undef, undef,
      $edited_bytes , undef, undef, undef, undef, undef) = stat($tmp_files{edited});
   my $lost_bytes = $rolling_bytes - $edited_bytes;
      my $lost_data = utils_convert_bytes_to_optimal_unit($lost_bytes);
   print "\tLost $lost_data because $tmp_files{editing}was corrupt.\n";
}

sub capinfo ($){
   my $capfile = shift;
   my @gather_cap_details = (
      "capinfos",
      $capfile
   );
   system (@gather_cap_details) == 0
      or clean_exit($?,"Error gathering capture details: @gather_cap_details: $!", $config{ignore_errors});
}

sub start() {
   my ($outfile, $abs_outfile, $abs_running_outfile) = generate_filenames("live");

   my @start_capture = (
      "tcpdump", 
      $config{tcpdump_flags},
      "-i",
      "$config{interface}",
      "-w",
      $abs_running_outfile,
      $config{bpf_expression}
   );
   if ($config{sudo}) {
      unshift(@start_capture, "sudo");
   }

   log_dump ("Executing: @start_capture");
   debug_print ("Executing @start_capture\n");
	
   # off we go
   system ("@start_capture &>/dev/null &") == 0
      or clean_exit($?, "Error starting the capture: @start_capture");
   print "Capturing to $abs_running_outfile\n";
}

sub find() {
   debug_print("I've been told to find captures");
   my @procs = `ps aux`
      or clean_exit($?,"Unable to find any running processes");
   my @found_procs;
   foreach my $found_process (@procs) {
      chomp $found_process;
      if ($found_process =~ /(tcpdump)|(daemonlogger)/) {
	 # this tends to be pretty portable
	 my (undef, $found_pid, undef, undef, undef, undef, undef, undef, undef, undef, $found_command) = split(/ +/, $found_process, 11);
	 # Ignore myself calling shell - only likely during backwards caps
	 next if ($found_command =~ /^sh/);
	 # Look for daemonlogger
	 if ($found_command =~ /daemonlogger/) {
	    # but ignore it if it doesn't match the filter
	    next unless ($found_command =~ /$config{list_filter}/);
	    $found_command =~ /-l\s(\S+)/;
	    my $found_ringbuffer_directory = $1;
	    $found_command =~ /-n\s(\S+)/;
	    my $found_ringbuffer_prefix = $1;
	    # Hairy because we are later going to store three other values here :(
	    # ugly hack workaround - static the pid to be 'daemon'
	    my @ps = ('daemonlogger', $found_ringbuffer_directory, $found_ringbuffer_prefix);
	    push (@found_procs, \@ps);
	    debug_print("I've found daemonlogger!\n" .
	       "\tDirectory: $found_ringbuffer_directory\n" .
	       "\tPrefix: $found_ringbuffer_prefix");
	    # No need to pass this further
	    next;
	 }
			
	 # Only keep captures matching the provided filter
	 if ($found_process =~ /$config{list_filter}/) {
	    $found_command =~ /-w\s+(.*)/;
	    my ($found_capfile, $found_bpf_expression) = split (/ /, $1, 2);
	    my @matching_ps = ($found_pid, $found_capfile, $found_bpf_expression);
	    push (@found_procs, \@matching_ps);
	    debug_print("I've found a pid, file, and bpf: @matching_ps");
	 }
      }
   }

   print "No captures found.\n" 
      if (!@found_procs);
   return @found_procs;
}

sub list() {
   debug_print("I've been told to list captures");
   my @found_procs = find();
   my (@found_targets, $found_pid, $found_capfile, $found_bpf_expression, 
      $rolling_progress, $progress, $rolling_analyst);
   my ($found_analyst, $found_description, $found_time);
   my $rolling_caps_size = 0;
   my $columns = "%-10.10s %-16.16s %-30.30s %-25.25s\n";
   if (@found_procs) {
      printf ($columns, "Analyst", "Created", "Descrition", "BPF") if (!$config{verbose});
   }
   while (@found_procs) {
      my $p = shift @found_procs;
      ($found_pid, $found_capfile, $found_bpf_expression) = (@$p[0], @$p[1], @$p[2]);
   # Pluck off any daemonlogger listings
      if ($found_pid eq 'daemonlogger') {
	 my $found_ringbuffer_directory = $found_capfile;
	 my $found_ringbuffer_prefix = $found_bpf_expression;
	 debug_print("Attempting to list daemonlogger");
	 $found_analyst = "daemon";
	 opendir ROLLINGCAPS, $found_ringbuffer_directory
	    or clean_exit($?,"could not open directory $config{rolling_cap_dir}: $!");
	 my @rolling_caps_dir = readdir ROLLINGCAPS;
	 my @rolling_caps;
	 closedir ROLLINGCAPS;
	 foreach my $file (@rolling_caps_dir) {
	    if ($file =~ /$found_ringbuffer_prefix/){
	       push (@rolling_caps, $file);
	       $file = catfile($found_ringbuffer_directory, $file);
	       my (undef, undef, undef, undef, undef, undef, undef,
		  $bytes , $atime, $mtime, $ctime, undef, undef) = stat($file);
	       $rolling_caps_size += $bytes;
	    }
	 }
	 @rolling_caps = sort @rolling_caps;
	 $found_capfile = catfile($found_ringbuffer_directory, shift(@rolling_caps));
	 my @file_parts = split(/\./, $found_capfile);
	 $found_time = pop(@file_parts);
	 $found_description = $found_ringbuffer_prefix;
	 $found_bpf_expression = "*";
      }
      else {
	 my ($found_volume,$found_directory,$found_file) = splitpath($found_capfile);
	 debug_print("I've found a file to work with: $found_file");
	 ($found_analyst, $found_description, undef, $found_time, undef) = split(/_/, $found_file);
	 ($found_time, undef)  = split(/\./, $found_time);
      }
   
      if ($found_analyst =~ /processing/) {
	 ($rolling_analyst, undef, undef, $rolling_progress) = split(/\./, $config{analyst});
	 $progress = scalar(localtime($rolling_progress));
	 $found_analyst = $rolling_analyst;
      }
      # Undo the conversion of spaces to dots in the filename for display
      $found_description =~ s/\./ /go;
      my (undef, undef, undef, undef, undef, undef, undef,
	 $bytes , $atime, $mtime, $ctime, undef, undef) = stat("$found_capfile")
	    or clean_exit($?,"$found_pid claims to be writing to $found_capfile, but I can't access it: $!");
      my $size = utils_convert_bytes_to_optimal_unit($bytes);
      my $rolling_caps_size = utils_convert_bytes_to_optimal_unit($rolling_caps_size);
      my $accessed = scalar(localtime($atime));
      my $modified = scalar(localtime($mtime));
      my $changed = scalar(localtime($ctime));
      my $created= scalar(localtime($found_time));
      my $short_created =  strftime "%b %e %H:%M:%S", localtime($found_time);
      if ($found_analyst =~ /daemon/) {
	 $size = "$rolling_caps_size (in $size pieces)";
      }
      if ($rolling_progress) {
	 if ($config{verbose}) {
	    print join ("\n",
	       "      Analyst: $found_analyst",
	       " Size (bytes): $size",
	       "     Progress: $progress",
	       "  Description: $found_description",
	       "   Expression: $found_bpf_expression",
	       " Capture File: $found_capfile\n\n") 
	 }
	 else {
	    printf ($columns, "* $found_analyst", $short_created, $found_description, $found_bpf_expression);
	 }
      }
      else {
	 if ($config{verbose}) {
	    print join ("\n",
	       "      Analyst: $found_analyst",
	       " Size (bytes): $size",
	       "      Started: $created",
	       "Last Modified: $modified",
	       "Last Accessed: $accessed",
	       " Last Changed: $changed",
	       "  Description: $found_description",
	       "   Expression: $found_bpf_expression",
	       " Capture File: $found_capfile\n\n")
	 }
	 else {
	    printf ($columns, $found_analyst, $short_created, $found_description, $found_bpf_expression);
	 }
      }
   }
}

sub stop(){
   debug_print("I've been told to stop captures");
   clean_exit($?,"You did not provide a filter, cowardly refusing to kill all captures")
      if (!$config{list_filter});
   log_dump("Stopping the following captures:\n");
   my @targets = find();
   while (@targets) {
      my ($p) = shift @targets;
      my ($found_pid, $found_capfile) = (@$p[0], @$p[1]);
      # don't try to kill daemonlogger
      if ($found_pid eq 'daemonlogger') {
	 verbose_print ("Cowardly refusing to kill daemonlogger");
	 next;
      }
      print "Stopping: $found_capfile\n";
      debug_print("I've been asked to kill pid: $found_pid");
      # there is a cleaner perl way to do this but not with sudo
      my @killer = (
	 "kill",
	 "$found_pid"
      );
      if ($config{sudo}) {
	 unshift(@killer, "sudo");
      }
      system (@killer) == 0
	 or clean_exit($?,"Error stopping capture: @killer");
      if ($config{calc_hash}) {
	 calc_hash($found_capfile);
	 move_complete ("$found_capfile.hashes");
      }
      move_complete($found_capfile);
   }
}

sub calc_hash($){
   debug_print("I've been asked to calculate hashes");
   my $file = shift;
   open(CAPTURE_FILE, $file) 
      or clean_exit($?,"Failed to open $file");
   binmode(CAPTURE_FILE);
   open(HASH_CONTAINER, ">$file.hashes")
      or clean_exit($?,"Failed to write to $file.hashes");
   my $sha1_digest = Digest::SHA1->new->addfile(*CAPTURE_FILE)->hexdigest;
   my $md5_digest = Digest::MD5->new->addfile(*CAPTURE_FILE)->hexdigest;
   close(CAPTURE_FILE);
   print HASH_CONTAINER 
      "SHA1: $sha1_digest\n" . 
      "MD5: $md5_digest\n";
   close(HASH_CONTAINER);
}
  
sub move_complete($){
   debug_print("I've been told to move files to " .$config{complete_cap_dir});
   my $file = shift;
   my ($source_volume,$source_directory,$source_file) = splitpath($file);
   move ($file, $config{complete_cap_dir})
      or clean_exit($?,"Error moving $file to $config{complete_cap_dir}");
   $tmp_files{complete_so_far} = catfile($config{complete_cap_dir}, $source_file);
   clean_up();
}

sub fixup_permissions($){
	debug_print("I've been told to fix permissions");
  my $file = shift;
  my @maintain_perms = (
    "chgrp",
    $config{group},
    $file
  );
  system (@maintain_perms) == 0
    or clean_exit($?,"Error updating permissions @maintain_perms", $file);
}

  
sub log_dump($) {
   debug_print("I've been asked to dump to syslog");
   if ($config{syslog}) {
      my $message = shift;
      openlog ("cap", 'PID', 'user')
	 or clean_exit($?,"Unable to open syslog");
      syslog("info", $message) 
	 or clean_exit($?,"Unable to write to system log");
      closelog();
   }
}

# Catch SIGINT.
sub signal_handler() {
   my $signal = shift;
   debug_print ("SSIGNAL: $signal\n");
   clean_exit($signal,"");
}

sub unlink_tmp_files {
   foreach my $file (@_) {
      if (-e $file) {
	 unlink $file
	    or print "Unable to remove junk $file\n";
	 debug_print("$file erased");
      }
   }
}

sub corrupt_or_exit {
   my ($questionmark, $exit_message, $ignore_errors) = @_;
   debug_print("Ignoring errors...\n".
		"\tExit:      $questionmark\n".
		"\tMessage:   $exit_message\n".
		"\tIgnore:    $ignore_errors");
   clean_exit($questionmark, $exit_message)
      if ($questionmark & 127 == 0);
   # assuming it was corrupt
   debug_print("Assuming we have a corrupt file and not a signal");
   fixcap();
   clean_exit($questionmark, $exit_message);
}

sub clean_exit {
   my ($questionmark, $exit_message, $ignore_errors) = @_;
   my ($exit_value, $signal_num, $dumped_core);
   if ($questionmark =~ /INT/) {
      print "Interrupted...\n";
      debug_print("Hashing");
      calc_hash($tmp_files{complete_so_far});
      clean_up();
      exit;
   }
   $exit_value = $questionmark >> 8;
   $signal_num = $questionmark & 127;
   $dumped_core = $questionmark & 128;
   if ($signal_num == 2) {
      print "\r\nInterrupted...\n";
      debug_print("Hashing");
      calc_hash($tmp_files{complete_so_far});
      clean_up();
      exit;
   }
   if ($ignore_errors) {
      return;
   }
   die("\nExiting...$exit_message\n".
      "\tExit value: $exit_value\n".
      "\tSignal number: $signal_num\n".
      "\tDumped core: $dumped_core\n");
}

sub clean_up {
   debug_print("Cleaning up...\n");
   while ((my $tmp_role, my $file) = each(%tmp_files)){
      debug_print ("\"$tmp_role\": $file\n")
	 if ($file);
   unlink_tmp_files($tmp_files{merging})
      if ($tmp_files{merging});
   unlink_tmp_files($tmp_files{filtering})
      if ($tmp_files{filtering});
   unlink_tmp_files($tmp_files{linked})
      if ($tmp_files{linked});
   unlink_tmp_files($tmp_files{merged})
      if ($tmp_files{merged});
   }

   # All done!
   if ($tmp_files{complete_so_far} =~ /$config{cap_extension}$/){
      print "pcap: $tmp_files{complete_so_far}\n";
      log_dump ("pcap: $tmp_files{complete_so_far}");
   }
   if ($config{calc_hash}) {
      print "hash: $tmp_files{complete_so_far}.hashes\n";
   }
   if ($config{verbose}) {
      capinfo($tmp_files{complete_so_far});
   }

}

sub utils_convert_bytes_to_optimal_unit($) {
   my($bytes) = @_;

   return '' if ($bytes eq '');

   my($size);
   $size = $bytes . ' Bytes' if ($bytes < 1024);
   $size = sprintf("%.2f", ($bytes/1024)) . ' KB' if ($bytes >= 1024 && $bytes < 1048576);
   $size = sprintf("%.2f", ($bytes/1048576)) . ' MB' if ($bytes >= 1048576 && $bytes < 1073741824);
   $size = sprintf("%.2f", ($bytes/1073741824)) . ' GB' if ($bytes >= 1073741824 && $bytes < 1099511627776);
   $size = sprintf("%.2f", ($bytes/1099511627776)) . ' TB' if ($bytes >= 1099511627776);

   return $size;
}

END{
  if(defined $config{versions}){
    print   "$VERSION",
      "  Modules, Perl, OS, Program info:\n",
      "  Pod::Usage            $Pod::Usage::VERSION\n",
      "  Getopt::Long          $Getopt::Long::VERSION\n",
      "  POSIX                 $POSIX::VERSION\n",
      "  strict                $strict::VERSION\n",
      "  Perl version          $]\n",
      "  Perl executable       $^X\n",
      "  OS                    $^O\n",
      "  $0\n",
      "\n\n";
  }
}

=head1 NAME

capture - capture is a tool for network security analysts to simplify starting, listing, and stopping full packet captures.

=head1 SYNOPSIS

capture [-h?lsmv] [r] [-a analyst -d 'quoted description' -e 'quoted expression'] [-f filter]

=head1 OPTIONS

=over 8

=item B<-a, --analyst>

When creating a new full packet capture file, capture will use the analyst name in the name of the capture file

=item B<-c, --checksum>

After stopping the capture, create an md5sum of the completed capture file

=item B<-d, --description>

When creating a new full packet capture file, capture will append the analyst provided description string to the file name

=item B<-e, --expression>

Follow the -e flag with quoted tcpdump syntax used to start the capture. 

=item B<-f, --filter>

When stopping or listing captures, a filter can be provided to display only matching content.

=item B<-h, --help>

Print Options and Argumetns

=item B<-l, --list>

List running captures

=item B<-m, --man>

Print complete man page

=item B<-r, --ringbuffer>

Acquite completed capture by reading and filtering ongoing daemonlogger traffic, and start a new capture with identical criteria

=item B<-s, --stop>

Stop captures based on filter

=item B<-v, --verbose>

Make all output verbose

=item B<-V, --Version>

Print version and exit

=item B<-z, --debug>

Print all debug output

=back

=head1 DESCRIPTION

Capture initiates, lists, and stops full packet captures using tcpdump while keeping analysts concerns on the incident at hand. It is only likley to work as expected in a GNU userland because of how it parses ps output.

Capture will automatically store full packet captures in a default active capture path and build the analyst name, time-stamp, and tcpdump expression directly into the filename.

Capture can also print active packet capture listings using filters.

Stopping captures provides analysts and automated programs the ability to create md5 sums of completed captures and moves completed captures to specified locations.

=head1 EXAMPLES

Starting a capture:

capture -a grant -d 'capturing all traffic to or from a host' -e 'host 10.10.10.10'

Listing captures:

capture -l

Verbosely listing captures started by an analyst named fred:

capture -lf fred

Stoping all of the caps found in the previous listing:

capture -sf fred

Listing captures described as torrent captures in the description:

capture -lf torrent

You should notice a trend - to stop the above listing of captures:

capture -sf torrent

To verbosely stop all captures described as irc captures and create .md5 checksums:

capture -svcf irc

=head1 ABOUT

capture is by Grant Stavely: grant.stavely@constellation.com

=cut

